Add Session Points: opt-in attendance scoring per organization#83
Add Session Points: opt-in attendance scoring per organization#83danielhauser wants to merge 9 commits into
Conversation
Adds two opt-in fields for the upcoming Session Points feature. Defaults (False, 1.00) preserve current behaviour for every org.
excused_users lets a member declare they are not coming without being counted as attending. point_weight_override and is_mandatory_override are nullable and fall back to the shift type when blank, so one-off events do not need a dedicated ShiftType.
attendance_points_enabled is the single boolean that gates the whole feature per-organisation. no_response_penalty is the org-wide constant deducted for unresponsive mandatory shifts.
Pure functions to resolve effective point weight and mandatory flag for a shift, and to aggregate per-member attended/excused/missed counts and the resulting saldo. Resolution falls back from a per-shift override to the shift type to a default.
ShiftTypeForm gains is_mandatory and point_weight. OrganizationShiftSummaryForm gains attendance_points_enabled and no_response_penalty.
point_weight_override and is_mandatory_override are added to the ShiftForm but popped at runtime when the bound organisation has Session Points disabled. Orgs without the feature see no extra fields on shift create/edit.
ExcuseSelfView adds the current user to Shift.excused_users and cleans up any existing Participant. WithdrawExcuseView removes them from the excused list. Both are gated by the existing participate_in_shift permission, POST only, idempotent.
Adds an inclusion tag that resolves the effective point weight and mandatory flag for a shift, and a small helper tag to check whether the current request user is on the excused list. The shift detail page shows the Session Points info line; the participants block shows an Excuse or Withdraw button depending on the user's state. All gated by attendance_points_enabled.
Extends member_shift_summary to compute per-member attended, excused, missed and points using the scoring utility. The summary template gains four columns rendered only when the organisation has Session Points enabled, and sorts the table by points descending in that case.
| help_text=_('Enable per-member Session Points tracking on the ' | ||
| 'shift summary and the shift detail page.')) | ||
| no_response_penalty = models.DecimalField(verbose_name=_('No-Response Penalty'), max_digits=4, decimal_places=2, | ||
| default=Decimal('0.33'), |
There was a problem hiding this comment.
I would suggest setting the default to something that does not impact the standard behavior
| default=Decimal('0.33'), | |
| default=Decimal('0.0'), |
| <div class="text-center"><h5>{% trans "Session Points (Override)" %}</h5></div> | ||
| <div class="center-items"> | ||
| <div class="col-6 me-1"> | ||
| {% bootstrap_field form.point_weight_override %} |
There was a problem hiding this comment.
It might be nicer to by default load the orgs default point weight here
| <i class="fa-solid fa-rotate-left me-2"></i>{% trans 'Withdraw excuse' %} | ||
| </button> | ||
| </form> | ||
| {% else %} |
There was a problem hiding this comment.
Is excusing oneself when you're neither a participant nor excused intended?
I would assume users should in this case get displayed nothing
| from shiftings.shifts.models import Shift | ||
|
|
||
|
|
||
| DEFAULT_POINT_WEIGHT = Decimal("1.00") |
There was a problem hiding this comment.
If we fallback to this global default it should probably be 0.
| DEFAULT_POINT_WEIGHT = Decimal("1.00") | |
| DEFAULT_POINT_WEIGHT = Decimal("0.00") |
| return DEFAULT_IS_MANDATORY | ||
|
|
||
|
|
||
| def member_score_data(shifts: Iterable["Shift"], user: "BaseUser", |
There was a problem hiding this comment.
Taking the iterable and filtering in python although elegant seems inefficient.
Doing this as far as possible directly in the query is likely more efficient and enables future use of technologies like indices and cashing on the Database level.
pablo-schmeiser
left a comment
There was a problem hiding this comment.
Thank you, @danielhauser, for this excellent PR.
Currently, I have been working on something similar and would like to check whether we can combine these 2 approaches.
I wanted to implement attendance tracking in a way where there is also a way to mark participants as present/absent:
- I had imagined people doing this with a checkmark in the attendance box.
- Users should also be able to mark participants as missing (checkmark should have 3 states).
- There should be permissions to do this to oneself and to do it to someone else. Overwrites might also be nice for this, but I am not sure if this is needed.
- This is enabled either globally on an org level (with a possible overwrite) or on a shift (by overwriting to enable it)
This would probably integrate well with your proposed changes.
@danielhauser how do you feel about this proposition?
This would allow organizations with more spontaneous/unreliable participants to track who signed up and who actually showed up (even allowing for something like a list of unreliable participants). I am currently working on events and would like to integrate this very behavior into the event views.
Overview
This PR adds an optional feature to shifts that allows organisations to track attendance and gives users the ability to excuse themselves. If a user neither attends nor formally excuses themselves, they receive a customisable, org-wide point penalty.
Motivation
At our organization (Belegungsausschuss), we currently rely on an awkward workaround to track attendance. Admins create two separate instances for every regular event: a positive one to signal attendance, and a negative one to act as an excuse. After the event, someone from the admin group has to manually verify attendance, track it in a separate Excel sheet, and calculate the difference between total events and the attended/excused users.
With this modification, we can completely get rid of these parallel structures and significantly reduce the administrative workload.
design
shift participants can now be in one of the following states:
The
no_response_penaltyonly applies when the shift was marked mandatory and the member didn't respond. I set it up org-wide we could also make that customizable per shift, but we don't need that in our work flow. I set the default 0.33 as this is what we are currently using. Excused members are neutral and we don't award points nor give them a penaltyFor the actual point values I went with a resolution chain so orgs don't have to set things up on every single shift. For any shift the effective weight comes from
shift.point_weight_overrideif it's set, otherwise fromshift.shift_type.point_weight, and falls back to a default of 1 if neither is set. The same logic applies for theis_mandatoryflag. So most shifts can just inherit from their type and an admin only has to override for special cases like a one-off training that should count double.The whole feature is gated behind a single toggle per organisation. As long as that toggle stays off none of the new UI elements show up, the override fields don't appear on the shift form, and the summary view looks the same as today. So existing orgs won't notice any change.
for the graphical integration I just added a new table. Screenshot below

and added new columns in the summary:

some notes:
excuse_others_from_shiftspermission if you'd prefer it.